#include "cdayview.h"

#include "ccalmanager.h"

#include <buola/image/cimage.h>
#include <buola/image/cgc.h>
#include <buola/image/cfont.h>
#include <buola/gui/cmenu.h>
#include <buola/gui/cconfig.h>
#include <buola/image/cpattern.h>
#include <buola/datetime/cdatetime.h>
#include <buola/widgets/cscrollbar.h>
#include <buola/functors/predicates/binary.h>
#include <buola/iterator/indirect.h>

namespace buola { namespace bcal {

CDayView::CDayView(CCalManager *pManager,int pNDays,int pFirstDay)
    :   mManager(pManager),
        mValid(VALID_NONE),
        mNDays(pNDays),
        mFirstDay(pFirstDay)
    ,   mScrollPos(0,0)
{
    mManager->sCalendarAdded.Connect(&CDayView::OnCalendarAdded,this,_1);

    for(int i=0;i<mNDays;i++)
        construct_back(mDays);

    mCreating.mActive=false;
    mMoving.mActive=false;

    SetBackColor(img::CColor(1,1,1));

    SetPointer(gui::CPointer::STOCK_DEFAULT);

    mFont=img::CFont::GetStock(L"menu");
    mSmallFont=img::CFont::GetStock(L"small");
    
    mScrollBar=new gui::CScrollBar;
    mScrollBar->SetValue(0,false);
    mScrollBar->eChanged.Connect(&CDayView::OnScroll,this,_1);

    Attach(mScrollBar,gui::ATTACH_EAST);
}

CDayView::~CDayView()
{
}

///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
////////////////////////// Interface //////////////////////////
///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////

void CDayView::SetDate(const CDate &pDate)
{
    mDate=pDate;
    if(mNDays>1)
    {
        mFirstDate=mDate.PreviousWeekDay(mFirstDay);
        if(mDate-mFirstDate>=mNDays) mFirstDate=mDate-(mNDays-1);
    }
    else
    {
        mFirstDate=pDate;
    }
    
    sChangeTitle(to_wstring(pDate.Day())+L' '+mDate.GetMonthYearText());

    Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
    Refresh();
}

void CDayView::SetToday()
{
    CDate lDate;
    lDate=CCalendarClock::now();
    SetDate(lDate);
}

void CDayView::Previous()
{
    SetDate(mDate-mNDays);
}

void CDayView::Next()
{
    SetDate(mDate+mNDays);
}

void CDayView::CalcEntries()
{
    CDate lDate(mFirstDate);

    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        lDay->mEntries.clear();

        for(auto lC=mManager->GetCalendars().begin();lC!=mManager->GetCalendars().end();++lC)
        {
            std::vector<CCalendarEvent*> lEvents;

            (*lC)->GetEventsBetween(lEvents,lDate.GetStartTime(),lDate.GetEndTime());

            img::CColor lLineColor=(*lC)->GetColor();
            img::CColor lFillColor=lLineColor*0.4+img::CColor(0.6,0.6,0.6,0.6);

            for(auto lEvent=lEvents.begin();lEvent!=lEvents.end();++lEvent)
            {
                if((*lEvent)->IsAllDay()) continue;
            
                SEntry &lEntry=construct_back(lDay->mEntries);
            
                lEntry.mEvent=*lEvent;
                lEntry.mLineColor=lLineColor;
                lEntry.mFillColor=lFillColor;
                
                if(mMoving.mActive&&mMoving.mEvent==*lEvent)
                {
                    lEntry.mLineColor.a=0.6;
                    lEntry.mFillColor.a=0.6;
                    lEntry.mMoving=true;
                }
                else
                {
                    lEntry.mMoving=false;
                }

                lEntry.mCreating=false;
            }
        }
    
        if(mCreating.mActive&&mCreating.mEvent&&
                mCreating.mEvent->IsBetween(lDate.GetStartTime(),lDate.GetEndTime()))
        {
            img::CColor lLineColor=mManager->GetCurrent()->GetColor();
            lLineColor.a=0.6;
            img::CColor lFillColor=lLineColor*0.6+img::CColor(0.6,0.6,0.6,0.36);

            SEntry &lEntry=construct_back(lDay->mEntries);
            
            lEntry.mEvent=mCreating.mEvent;
            lEntry.mLineColor=lLineColor;
            lEntry.mFillColor=lFillColor;
            lEntry.mCreating=true;
            lEntry.mMoving=false;
        }

        lDate++;
    }

    mValid|=VALID_ENTRIES;
}

void CDayView::CalcSlots()
{
    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        std::sort(lDay->mEntries.begin(),lDay->mEntries.end(),indirect(fn::less()));

        //assign slots
        std::vector<SEntry*> lAllUnfinished;
        std::vector<SEntry*> lUnfinished;
    
        std::vector<bool> lSlots;

        for(auto lEntry=indirect_begin(lDay->mEntries);lEntry!=indirect_end(lDay->mEntries);++lEntry)
        {
            if(lEntry->mCreating)
            {
                lEntry->mSlot=0;
                lEntry->mNumSlots=1;
                continue;
            }

            if(lEntry->mMoving)
            {
                lEntry->mSlot=mMoving.mSlot;
                lEntry->mNumSlots=mMoving.mNumSlots;
                continue;
            }
            
            for(int j=0;j<lUnfinished.size();j++)
            {
                if(lUnfinished[j]->mEvent->GetEnd()<=lEntry->mEvent->GetStart())
                {
                    lSlots[lUnfinished[j]->mSlot]=false;
                    lUnfinished.erase(lUnfinished.begin()+j);
                    j--;
                }
            }
        
            if(lUnfinished.empty())
            {
                for(int j=0;j<lAllUnfinished.size();j++)
                {
                    lAllUnfinished[j]->mNumSlots=lSlots.size();
                }
                
                lSlots.clear();
                lAllUnfinished.clear();
            }

            int lSlot;

            for(lSlot=0;lSlot<lSlots.size();lSlot++)
            {
                if(!lSlots[lSlot])
                {
                    lSlots[lSlot]=true;
                    break;
                }
            }
        
            if(lSlot>=lSlots.size()) //no empty slot found
                lSlots.push_back(true);

            lEntry->mSlot=lSlot;

            lUnfinished.push_back(&*lEntry);
            lAllUnfinished.push_back(&*lEntry);
        }

        for(int i=0;i<lAllUnfinished.size();i++)
            lAllUnfinished[i]->mNumSlots=lSlots.size();
    }
    
    mValid|=VALID_SLOTS;
}

void CDayView::CalcShape()
{
    mRows=12;
    mHoursRect={45,0,Size().x,Size().y};

    double lColWidth=mHoursRect.w()/mNDays;
    double lColX=mHoursRect.l;

    mRowHeight=mHoursRect.h()/mRows;

    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        lDay->mXL=lColX;
        lColX+=lColWidth;
        lDay->mXR=lColX;
    }

    mScrollBar->SetRange(0,mRowHeight*(24-mRows));
    mScrollBar->SetStep(mRowHeight,mHoursRect.h());

    mValid|=VALID_SHAPE;
}

void CDayView::CalcPositions()
{
    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        for(auto lEntry=indirect_begin(lDay->mEntries);lEntry!=indirect_end(lDay->mEntries);++lEntry)
        {
            CDateTime lStart(lEntry->mEvent->GetStart());
            CDateTime lEnd(lEntry->mEvent->GetEnd());
        
            lEntry->mRect.t=round(mHoursRect.t+(lStart.Time().Hour()+lStart.Time().Min()/60.0)*mRowHeight)+0.5;
            lEntry->mRect.b=round(mHoursRect.t+(lEnd.Time().Hour()+lEnd.Time().Min()/60.0)*mRowHeight)-0.5;

            double lW=round((lDay->mXR-lDay->mXL)/lEntry->mNumSlots);
            lEntry->mRect.l=round(lDay->mXL+lW*lEntry->mSlot)+5.5;
            lEntry->mRect.r=round(lDay->mXL+lW*(1+lEntry->mSlot))-5.5;
        }
    }
    
    mValid|=VALID_POSITIONS;
}

void CDayView::MakeValid(EValid pWhat)
{
    if((pWhat&VALID_ENTRIES)&&!(mValid&VALID_ENTRIES))
        CalcEntries();
    if((pWhat&VALID_SLOTS)&&!(mValid&VALID_SLOTS))
        CalcSlots();
    if((pWhat&VALID_SHAPE)&&!(mValid&VALID_SHAPE))
        CalcShape();
    if((pWhat&VALID_POSITIONS)&&!(mValid&VALID_POSITIONS))
        CalcPositions();
}
    
CDayView::SEntry *CDayView::GetEntryAt(const CPoint_d &pP)
{
    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        for(auto lEntry=indirect_begin(lDay->mEntries);lEntry!=indirect_end(lDay->mEntries);++lEntry)
        {
            if(lEntry->mRect.Contains(pP))
                return &*lEntry;
        }
    }
    
    return nullptr;
}

CDateTime CDayView::GetDateTimeAt(const CPoint_d &pP)
{
    CDate lDate;
    
    int i=0;
    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        if(pP.x>=lDay->mXL&&pP.x<=lDay->mXR)
        {
            lDate=mFirstDate+i;
            break;
        }
        i++;
    }
    
    if(lDate.IsNull()) return CDateTime();

    int lMinutes=(int)((pP.y-mHoursRect.t)/mRowHeight*60.0);

    return CDateTime(lDate,CTimeOfDay(chrono::minutes(lMinutes)));
}

///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
/////////////////////// Event handlers ////////////////////////
///////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////

void CDayView::OnPreExpose()
{
    MakeValid(VALID_ENTRIES|VALID_SLOTS|VALID_SHAPE|VALID_POSITIONS);
}
    
void CDayView::OnExpose(const gui::CExposeEvent &pE)
{
    img::CGC *lGC=pE.mGC;

    lGC->SetFont(mFont);

    lGC->SetLineWidth(1.0);

    lGC->SetOperator(img::ECompOp::OVER);

    for(int i=0;i<24;i++)
    {
        double lY=mHoursRect.t+round(i*mRowHeight)-mScrollPos.y;

        if(lY+mRowHeight<mHoursRect.t) continue;
        if(lY>mHoursRect.b) continue;

        if(lY>mHoursRect.t+mFont->GetHeight()/2&&lY<mHoursRect.b-mFont->GetHeight()/2)
        {
            std::wstring lString=to_wstring(i);
            lString+=L":00";

            lGC->SetSource(img::CColor(0.4,0.4,0.4));
            lGC->DrawText(CPoint_d(mHoursRect.l-5,lY),lString,img::ETextPos::RIGHT|img::ETextPos::VCENTER);
        }
        
        lGC->SetSource(img::CColor(0.91,0.93,0.97));
        lGC->Path().Rect(CPoint_d(mHoursRect.l,lY+0.5),CSize_d(mHoursRect.w(),mRowHeight));
        lGC->FillPath();
        lGC->SetSource(img::CColor(0.76,0.76,0.76));
        lGC->Path().MoveTo(CPoint_d(mHoursRect.l,lY+0.5));
        lGC->Path().LineTo(CPoint_d(mHoursRect.r,lY+0.5));
        lGC->StrokePath();
        lGC->SetSource(img::CColor(0.8,0.8,0.8));
        lGC->Path().MoveTo(CPoint_d(mHoursRect.l,lY+0.5+round(mRowHeight/2)));
        lGC->Path().LineTo(CPoint_d(mHoursRect.r,lY+0.5+round(mRowHeight/2)));
        lGC->StrokePath();
    }

    //draw events
    for(auto lDay=indirect_begin(mDays);lDay!=indirect_end(mDays);++lDay)
    {
        lGC->SetSource(img::CColor(0.8,0.8,0.8));
    
        lGC->Path().MoveTo(CPoint_d(round(lDay->mXL)+0.5,mHoursRect.t));
        lGC->Path().LineTo(CPoint_d(round(lDay->mXL)+0.5,mHoursRect.b));
        lGC->StrokePath();

        for(auto lEntry=indirect_begin(lDay->mEntries);lEntry!=indirect_end(lDay->mEntries);++lEntry)
        {
            CDateTime lStart(lEntry->mEvent->GetStart());
            CDateTime lEnd(lEntry->mEvent->GetEnd());
        
            CRect_d lRect=lEntry->mRect-mScrollPos;

            lGC->SetLineWidth(1.0);
            lGC->Path().RoundRect(lRect,5.0);
            img::CLinearGradient lPattern(CPoint_d(lRect.l,lRect.t),CPoint_d(lRect.l,lRect.b));
            double lAlpha=lEntry->mLineColor.a;
            img::CColor lGColor0a=lEntry->mLineColor*0.5+img::CColor(0.5,0.5,0.5,0.5*lAlpha);
            img::CColor lGColor0b=lEntry->mLineColor*0.6+img::CColor(0.4,0.4,0.4,0.4*lAlpha);
            img::CColor lGColor1=lEntry->mLineColor*0.4+img::CColor(0.6,0.6,0.6,0.6*lAlpha);
            img::CColor lGColor2=lEntry->mLineColor*0.2+img::CColor(0.8,0.8,0.8,0.8*lAlpha);
            lPattern.AddStop(lGColor0a,0.0);
            lPattern.AddStop(lGColor0b,16.0/(lRect.h()));
            lPattern.AddStop(lGColor1,16.0/(lRect.h()));
            lPattern.AddStop(lGColor2,1.0);

            lGC->SetSource(lPattern);
            lGC->FillPath(true);
            lGC->SetSource(lEntry->mLineColor);
            lGC->StrokePath();

            lGC->SetSource(img::CColor(0,0,0));
            lGC->SetFont(mFont);
            lGC->DrawText(CPoint_d(lRect.l+5,lRect.t+8),
                    lStart.Time().GetHourMinText()+L" - "+lEnd.Time().GetHourMinText(),img::ETextPos::VCENTER);

            lGC->DrawText(CPoint_d(lRect.l+5,lRect.t+18),
                                            lEntry->mEvent->GetSummary());

            lGC->SetFont(mSmallFont);
            lGC->DrawText(CPoint_d(lRect.l+5,lRect.t+20+mFont->GetHeight()),
                                lEntry->mEvent->GetDescription());
        }
    }
}    

void CDayView::OnButtonDown(const gui::CMouseEvent &pE)
{
    if(pE.mButton==gui::EMouseButton::LEFT)
    {
        CPoint_d lP=pE.mPoint+mScrollPos;
    
        MakeValid(VALID_ENTRIES|VALID_SLOTS|VALID_SHAPE|VALID_POSITIONS);

        SEntry *lEntry=GetEntryAt(lP);
        
        if(lEntry)
        {
            mMoving.mActive=true;
            mMoving.mStart=CDateTime(lEntry->mEvent->GetStart());
            mMoving.mAnchor=CPoint_d(lP.x,lEntry->mRect.t);
            mMoving.mPoint=lP;
            mMoving.mEvent=lEntry->mEvent;
            mMoving.mSlot=lEntry->mSlot;
            mMoving.mNumSlots=lEntry->mNumSlots;
            return;
        }

        CDateTime lDT=GetDateTimeAt(lP);
        
        if(!lDT.IsNull())
        {
            int lMinutes=lDT.Time().Min();
            lMinutes=((lMinutes+7)/15)*15;
            lDT.Time().SetMin(lMinutes);
            lDT.Normalize();

            mCreating.mActive=true;
            mCreating.mStart=lDT;
            mCreating.mEnd=lDT;
            mCreating.mEvent=nullptr;
        }
    }
}

void CDayView::OnButtonUp(const gui::CMouseEvent &pE)
{
    if(pE.mButton==gui::EMouseButton::LEFT)
    {
        if(mCreating.mActive)
        {
            if(mCreating.mEvent)
            {
                mManager->GetCurrent()->AddEvent(mCreating.mEvent);

                Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
                Refresh();
            }
            
            mCreating.mActive=false;
        }
        else if(mMoving.mActive)
        {
            mMoving.mActive=false;

            Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
            Refresh();
        }
    }
}

void CDayView::OnMouseMove(const gui::CMouseEvent &pE)
{
    if(mCreating.mActive)
    {
        CDateTime lDT=GetDateTimeAt(pE.mPoint+mScrollPos);
        
        if(!lDT.IsNull())
        {
            int lMinutes=lDT.Time().Min();
            lMinutes=((lMinutes+7)/15)*15;

            lDT.Time().SetMin(lMinutes);
            lDT.Normalize();

            if(lDT==mCreating.mEnd) return;
            
            mCreating.mEnd=lDT;
            
            if(mCreating.mStart!=mCreating.mEnd)
            {
                if(!mCreating.mEvent)
                {
                    mCreating.mEvent=new CCalendarEvent(std::min(mCreating.mStart,mCreating.mEnd).GetCalendarTime(),
                                                        std::max(mCreating.mStart,mCreating.mEnd).GetCalendarTime());

                    Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
                }
                else
                {
                    mCreating.mEvent->SetStart(std::min(mCreating.mStart,mCreating.mEnd).GetCalendarTime());
                    mCreating.mEvent->SetEnd(std::max(mCreating.mStart,mCreating.mEnd).GetCalendarTime());
                    
                    Invalidate(VALID_POSITIONS);
                }

                Refresh();
            }
            else
            {
                if(mCreating.mEvent)
                {
                    delete mCreating.mEvent;
                    mCreating.mEvent=nullptr;

                    Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
                    Refresh();
                }
            }
        }
    }
    else if(mMoving.mActive)
    {
        CPoint_d lDest=mMoving.mAnchor+(pE.mPoint+mScrollPos)-mMoving.mPoint;

        CDateTime lDT=GetDateTimeAt(lDest);
        
        if(!lDT.IsNull())
        {
            int lMinutes=lDT.Time().Min();
            lMinutes=((lMinutes+7)/15)*15;

            lDT.Time().SetMin(lMinutes);
            lDT.Normalize();

            if(lDT!=mMoving.mStart)
            {
                CCalendarTime lTime=lDT.GetCalendarTime();
                auto lDuration=mMoving.mEvent->GetEnd()-mMoving.mEvent->GetStart();
                mMoving.mEvent->SetStart(lTime);
                mMoving.mEvent->SetEnd(lTime+lDuration);
                mMoving.mStart=lDT;

                Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
                Refresh();
            }
        }
    }
}

void CDayView::OnMouseWheel(const gui::CMouseEvent &pE)
{
    if(pE.mScrollY!=0.0)
    {
        mScrollBar->Scroll(pE.mScrollY);
    }
}

void CDayView::OnSize(const gui::CGeometryEvent &pE)
{
    Invalidate(VALID_SHAPE|VALID_POSITIONS);
}

void CDayView::OnStyleChanged(TStyle)
{
//  Refresh();
}

void CDayView::OnScroll(int pPos)
{
    mScrollPos.y=pPos;
    Refresh();
}

void CDayView::OnCalendarAdded(CCalendar *pCalendar)
{
    Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
    Refresh();
}

void CDayView::OnCalendarRemoved(CCalendar *pCalendar)
{
    Invalidate(VALID_ENTRIES|VALID_SLOTS|VALID_POSITIONS);
    Refresh();
}

/*namespace buola*/ } /*namespace bcal*/ }
